Susipažinkite su lygiagretaus žemėlapio (Concurrent Map) koncepcija JavaScript, skirta lygiagrečioms duomenų struktūrų operacijoms, gerinančioms našumą daugiagijėse ar asinchroninėse aplinkose. Sužinokite apie privalumus, diegimo iššūkius ir praktinius panaudojimo atvejus.
JavaScript lygiagretusis žemėlapis (Concurrent Map): lygiagrečios duomenų struktūros operacijos našumui didinti
Šiuolaikiniame JavaScript kūrime, ypač Node.js aplinkose ir naršyklėse, naudojančiose Web Workers, gebėjimas atlikti lygiagrečias operacijas tampa vis svarbesnis. Viena iš sričių, kur lygiagretumas ženkliai veikia našumą, yra duomenų struktūrų manipuliavimas. Šiame tinklaraščio įraše gilinamasi į lygiagretaus žemėlapio (Concurrent Map) koncepciją JavaScript, galingą įrankį lygiagrečioms duomenų struktūros operacijoms, galintį dramatiškai pagerinti programos našumą.
Lygiagrečių duomenų struktūrų poreikio supratimas
Tradicinės JavaScript duomenų struktūros, tokios kaip integruota Map ir Object, iš prigimties yra vienagijės. Tai reiškia, kad vienu metu tik viena operacija gali pasiekti ar keisti duomenų struktūrą. Nors tai supaprastina programos elgsenos supratimą, tai gali tapti kliūtimi scenarijuose, apimančiuose:
- Daugiagijės aplinkos: Naudojant Web Workers JavaScript kodui vykdyti lygiagrečiose gijose, prieiga prie bendro
Mapiš kelių darbuotojų vienu metu gali sukelti lenktynių sąlygas (race conditions) ir duomenų sugadinimą. - Asinchroninės operacijos: Node.js ar naršyklės programose, dirbančiose su daugybe asinchroninių užduočių (pvz., tinklo užklausos, failų I/O), keli atgalinio iškvietimo (callback) metodai gali bandyti modifikuoti
Maplygiagrečiai, sukeldami nenuspėjamą elgseną. - Aukšto našumo programos: Programos, kurioms reikalingas intensyvus duomenų apdorojimas, pavyzdžiui, realaus laiko duomenų analizė, žaidimų kūrimas ar mokslinės simuliacijos, gali gauti naudos iš lygiagrečių duomenų struktūrų teikiamo paralelizmo.
Lygiagretusis žemėlapis sprendžia šias problemas, suteikdamas mechanizmus saugiai pasiekti ir modifikuoti žemėlapio turinį iš kelių gijų ar asinchroninių kontekstų lygiagrečiai. Tai leidžia lygiagrečiai vykdyti operacijas, o tai tam tikruose scenarijuose lemia reikšmingą našumo padidėjimą.
Kas yra lygiagretusis žemėlapis?
Lygiagretusis žemėlapis yra duomenų struktūra, leidžianti kelioms gijoms ar asinchroninėms operacijoms lygiagrečiai pasiekti ir modifikuoti jos turinį, nesukeliant duomenų sugadinimo ar lenktynių sąlygų. Tai paprastai pasiekiama naudojant:
- Atominės operacijos: Operacijos, kurios vykdomos kaip vienas, nedalomas vienetas, užtikrinant, kad jokia kita gija negalėtų įsikišti operacijos metu.
- Užrakinimo mechanizmai: Metodai, tokie kaip mutex'ai ar semaforai, kurie leidžia vienu metu tik vienai gijai pasiekti tam tikrą duomenų struktūros dalį, taip išvengiant lygiagrečių modifikacijų.
- Duomenų struktūros be užraktų: Pažangios duomenų struktūros, kurios visiškai išvengia aiškaus užrakinimo, naudodamos atomines operacijas ir sumanius algoritmus duomenų nuoseklumui užtikrinti.
Specifinės lygiagretaus žemėlapio diegimo detalės skiriasi priklausomai nuo programavimo kalbos ir pagrindinės aparatinės įrangos architektūros. JavaScript kalboje įdiegti tikrai lygiagrečią duomenų struktūrą yra sudėtinga dėl kalbos vienagijės prigimties. Tačiau mes galime imituoti lygiagretumą naudodami tokius metodus kaip Web Workers ir asinchronines operacijas, kartu su atitinkamais sinchronizavimo mechanizmais.
Lygiagretumo imitavimas JavaScript naudojant Web Workers
Web Workers suteikia galimybę vykdyti JavaScript kodą atskirose gijose, leidžiant mums imituoti lygiagretumą naršyklės aplinkoje. Panagrinėkime pavyzdį, kuriame norime atlikti skaičiavimams imlias operacijas su dideliu duomenų rinkiniu, saugomu Map.
Pavyzdys: lygiagretus duomenų apdorojimas su Web Workers ir bendru žemėlapiu
Tarkime, turime Map su vartotojų duomenimis ir norime apskaičiuoti vidutinį vartotojų amžių kiekvienoje šalyje. Galime padalinti duomenis tarp kelių Web Workers ir leisti kiekvienam darbuotojui lygiagrečiai apdoroti dalį duomenų.
Pagrindinė gija (index.html arba main.js):
// Sukuriamas didelis vartotojų duomenų žemėlapis
const userData = new Map();
for (let i = 0; i < 10000; i++) {
const country = ['USA', 'Canada', 'UK', 'Germany', 'France'][i % 5];
userData.set(i, { age: Math.floor(Math.random() * 60) + 18, country });
}
// Duomenys padalijami į dalis kiekvienam darbuotojui
const numWorkers = 4;
const chunkSize = Math.ceil(userData.size / numWorkers);
const dataChunks = [];
let i = 0;
for (let j = 0; j < numWorkers; j++) {
const chunk = new Map();
let count = 0;
for (; i < userData.size && count < chunkSize; i++) {
chunk.set(i, userData.get(i));
count++;
}
dataChunks.push(chunk);
}
// Sukuriami Web Workers
const workers = [];
const results = new Map();
let completedWorkers = 0;
for (let i = 0; i < numWorkers; i++) {
const worker = new Worker('worker.js');
workers.push(worker);
worker.onmessage = (event) => {
const { countryAverages } = event.data;
// Rezultatų sujungimas iš darbuotojo
for (const [country, average] of countryAverages) {
if (results.has(country)) {
const existing = results.get(country);
results.set(country, { sum: existing.sum + average.sum, count: existing.count + average.count });
} else {
results.set(country, average);
}
}
completedWorkers++;
if (completedWorkers === numWorkers) {
// Visi darbuotojai baigė darbą
const finalAverages = new Map();
for (const [country, data] of results) {
finalAverages.set(country, data.sum / data.count);
}
console.log('Final Averages:', finalAverages);
}
worker.terminate(); // Darbuotojo nutraukimas po naudojimo
};
worker.onerror = (error) => {
console.error('Worker error:', error);
};
// Duomenų dalies siuntimas darbuotojui
worker.postMessage({ data: Array.from(dataChunks[i]) });
}
Web Worker (worker.js):
self.onmessage = (event) => {
const { data } = event.data;
const userData = new Map(data);
const countryAverages = new Map();
for (const [id, user] of userData) {
const { country, age } = user;
if (countryAverages.has(country)) {
const existing = countryAverages.get(country);
countryAverages.set(country, { sum: existing.sum + age, count: existing.count + 1 });
} else {
countryAverages.set(country, { sum: age, count: 1 });
}
}
self.postMessage({ countryAverages: countryAverages });
};
Šiame pavyzdyje kiekvienas Web Worker apdoroja savo nepriklausomą duomenų kopiją. Tai leidžia išvengti būtinybės naudoti aiškius užrakinimo ar sinchronizavimo mechanizmus. Tačiau rezultatų sujungimas pagrindinėje gijoje vis tiek gali tapti kliūtimi, jei darbuotojų skaičius ar sujungimo operacijos sudėtingumas yra didelis. Tokiu atveju galite apsvarstyti galimybę naudoti tokius metodus kaip:
- Atominiai atnaujinimai: Jei agregavimo operaciją galima atlikti atomiškai, galite naudoti SharedArrayBuffer ir Atomics operacijas, kad atnaujintumėte bendrą duomenų struktūrą tiesiogiai iš darbuotojų. Tačiau šiam metodui reikalinga kruopšti sinchronizacija ir jis gali būti sudėtingas teisingai įgyvendinti.
- Pranešimų perdavimas: Užuot sujungus rezultatus pagrindinėje gijoje, galite leisti darbuotojams siųsti dalinius rezultatus vieni kitiems, paskirstant sujungimo krūvį tarp kelių gijų.
Paprasto lygiagretaus žemėlapio įgyvendinimas su asinchroninėmis operacijomis ir užraktais
Nors Web Workers suteikia tikrą paralelumą, mes taip pat galime imituoti lygiagretumą naudodami asinchronines operacijas ir užrakinimo mechanizmus vienoje gijoje. Šis metodas ypač naudingas Node.js aplinkose, kur dažnai pasitaiko su I/O susijusių operacijų.
Štai paprastas lygiagretaus žemėlapio pavyzdys, įgyvendintas naudojant paprastą užrakinimo mechanizmą:
class ConcurrentMap {
constructor() {
this.map = new Map();
this.lock = false; // Paprastas užraktas naudojant loginę vėliavėlę
}
async get(key) {
while (this.lock) {
// Laukiama, kol užraktas bus atleistas
await new Promise((resolve) => setTimeout(resolve, 0));
}
return this.map.get(key);
}
async set(key, value) {
while (this.lock) {
// Laukiama, kol užraktas bus atleistas
await new Promise((resolve) => setTimeout(resolve, 0));
}
this.lock = true; // Užrakto įgijimas
try {
this.map.set(key, value);
} finally {
this.lock = false; // Užrakto atleidimas
}
}
async delete(key) {
while (this.lock) {
// Laukiama, kol užraktas bus atleistas
await new Promise((resolve) => setTimeout(resolve, 0));
}
this.lock = true; // Užrakto įgijimas
try {
this.map.delete(key);
} finally {
this.lock = false; // Užrakto atleidimas
}
}
}
// Naudojimo pavyzdys
async function example() {
const concurrentMap = new ConcurrentMap();
// Lygiagrečios prieigos imitavimas
const promises = [];
for (let i = 0; i < 10; i++) {
promises.push(
(async () => {
await concurrentMap.set(i, `Value ${i}`);
console.log(`Set ${i}:`, await concurrentMap.get(i));
await concurrentMap.delete(i);
console.log(`Deleted ${i}:`, await concurrentMap.get(i));
})()
);
}
await Promise.all(promises);
console.log('Finished!');
}
example();
Šis pavyzdys naudoja paprastą loginę vėliavėlę kaip užraktą. Prieš pasiekdama ar modifikuodama Map, kiekviena asinchroninė operacija laukia, kol užraktas bus atleistas, įgyja užraktą, atlieka operaciją ir tada atleidžia užraktą. Tai užtikrina, kad vienu metu prie Map gali prieiti tik viena operacija, taip išvengiant lenktynių sąlygų.
Svarbi pastaba: Tai labai paprastas pavyzdys ir neturėtų būti naudojamas gamybinėse aplinkose. Jis yra labai neefektyvus ir jautrus tokioms problemoms kaip aklavietės (deadlocks). Realiose programose turėtų būti naudojami patikimesni užrakinimo mechanizmai, pavyzdžiui, semaforai ar mutex'ai.
Iššūkiai ir svarstymai
Lygiagretaus žemėlapio įgyvendinimas JavaScript kalboje kelia keletą iššūkių:
- JavaScript vienagijė prigimtis: JavaScript iš esmės yra vienagijė, o tai riboja tikrojo paralelizmo lygį, kurį galima pasiekti. Web Workers suteikia būdą apeiti šį apribojimą, tačiau jie sukelia papildomo sudėtingumo.
- Sinchronizacijos pridėtinės išlaidos: Užrakinimo mechanizmai sukelia pridėtines išlaidas, kurios gali panaikinti lygiagretumo teikiamą našumo naudą, jei nebus kruopščiai įgyvendintos.
- Sudėtingumas: Lygiagrečių duomenų struktūrų projektavimas ir įgyvendinimas yra iš prigimties sudėtingas ir reikalauja gilaus lygiagretumo koncepcijų ir galimų spąstų supratimo.
- Derinimas (Debugging): Lygiagretaus kodo derinimas gali būti žymiai sudėtingesnis nei vienagijo kodo derinimas dėl nedeterministinės lygiagretaus vykdymo prigimties.
Lygiagrečių žemėlapių panaudojimo atvejai JavaScript
Nepaisant iššūkių, lygiagretieji žemėlapiai gali būti naudingi keliais scenarijais:
- Spartinančioji atmintinė (Caching): Lygiagrečios spartinančiosios atmintinės įgyvendinimas, kurią galima pasiekti ir atnaujinti iš kelių gijų ar asinchroninių kontekstų.
- Duomenų agregavimas: Duomenų agregavimas iš kelių šaltinių lygiagrečiai, pavyzdžiui, realaus laiko duomenų analizės programose.
- Užduočių eilės: Užduočių eilės valdymas, kurias gali lygiagrečiai apdoroti keli darbuotojai.
- Žaidimų kūrimas: Žaidimo būsenos valdymas lygiagrečiai kelių žaidėjų žaidimuose.
Alternatyvos lygiagretiems žemėlapiams
Prieš įgyvendindami lygiagretųjį žemėlapį, apsvarstykite, ar alternatyvūs metodai galėtų būti tinkamesni:
- Nekintamos duomenų struktūros (Immutable Data Structures): Nekintamos duomenų struktūros gali panaikinti užrakinimo poreikį, užtikrindamos, kad duomenys negali būti modifikuoti po jų sukūrimo. Bibliotekos, tokios kaip Immutable.js, suteikia nekintamas duomenų struktūras JavaScript.
- Pranešimų perdavimas: Pranešimų perdavimo naudojimas komunikacijai tarp gijų ar asinchroninių kontekstų gali visiškai išvengti bendros kintamos būsenos poreikio.
- Skaičiavimų perkėlimas: Skaičiavimams imlių užduočių perkėlimas į serverines paslaugas (backend) ar debesijos funkcijas gali atlaisvinti pagrindinę giją ir pagerinti programos reakciją.
Išvada
Lygiagretieji žemėlapiai suteikia galingą įrankį lygiagrečioms duomenų struktūros operacijoms JavaScript kalboje. Nors jų įgyvendinimas kelia iššūkių dėl JavaScript vienagijės prigimties ir lygiagretumo sudėtingumo, jie gali žymiai pagerinti našumą daugiagijėse ar asinchroninėse aplinkose. Suprasdami kompromisus ir atidžiai apsvarstydami alternatyvius metodus, kūrėjai gali pasinaudoti lygiagrečiais žemėlapiais kurdami efektyvesnes ir labiau keičiamo dydžio JavaScript programas.
Nepamirškite kruopščiai išbandyti ir įvertinti savo lygiagretaus kodo našumą, kad užtikrintumėte, jog jis veikia teisingai ir kad našumo nauda viršija sinchronizacijos pridėtines išlaidas.
Tolesnis tyrinėjimas
- Web Workers API: MDN Web Docs
- SharedArrayBuffer and Atomics: MDN Web Docs
- Immutable.js: Oficiali svetainė